using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;


namespace datorgrafik_2
{
    /// <summary>
    /// This is a game component that implements IUpdateable.
    /// </summary>
    public class Sphere : Microsoft.Xna.Framework.GameComponent
    {
        private VertexPositionNormalTexture[] vertices;
        private int[] indices;

        private VertexBuffer vb;
        private IndexBuffer ib;

        private Game parent;

        int heigth;
        int width;

        private float radius = 50f;
        private Matrix objectWorld;

        Random r = new Random();
        private HSL[,] heightData;
        private Texture2D heightMap;

        private float SphereScale = 1.0f;


        /// <summary>
        /// Initializes a new instance of the <see cref="Sphere" /> class.
        /// </summary>
        /// <param name="game">The game.</param>
        /// <param name="heightMap">The height map.</param>
        public Sphere(Game game, Texture2D heightMap, float radius)
            : base(game)
        {
            // TODO: Construct any child components here
            if (radius > 0) this.radius = radius;
            this.parent = game;
            this.heightMap = heightMap;
            LoadHeightData(heightMap);
        }

        /// <summary>
        /// Allows the game component to perform any initialization it needs to before starting
        /// to run.  This is where it can query for any required services and load content.
        /// </summary>
        public override void Initialize()
        {
            // TODO: Add your initialization code here
            heigth = heightMap.Height;
            width = heightMap.Width;
            InitializeVertices(heigth, width);
            InitializeIndices(heigth, width);
            FixNormals();

            Spherify(radius, width, heigth);
            SetData();
        }

        /// <summary>
        /// Sets the data.
        /// </summary>
        private void SetData()
        {
            // vertexbuffer
            vb = new VertexBuffer(parent.GraphicsDevice, typeof(VertexPositionNormalTexture), vertices.Length, BufferUsage.WriteOnly);
            vb.SetData<VertexPositionNormalTexture>(vertices);
            // indexbuffer
            ib = new IndexBuffer(parent.GraphicsDevice, typeof(int), indices.Length, BufferUsage.WriteOnly);
            ib.SetData<int>(indices);

            objectWorld = Matrix.Identity;
            base.Initialize();
        }

        /// <summary>
        /// Initializes the vertices.
        /// </summary>
        /// <param name="heigth">The heigth.</param>
        /// <param name="width">The width.</param>
        private void InitializeVertices(int heigth, int width)
        {
            vertices = new VertexPositionNormalTexture[(heigth * width)];
            int vertexIndex = 0;
            for (int i = 0; i < heigth; i++)
            {
                for (int j = 0; j < width; j++)
                {
                    vertices[vertexIndex] = new VertexPositionNormalTexture(new Vector3(j, i, -(float)heightData[j, i].h / 100f), new Vector3(), new Vector2());
                    vertices[vertexIndex].TextureCoordinate.X = (float)j / width;
                    vertices[vertexIndex].TextureCoordinate.Y = (float)i / heigth;
                    vertexIndex++;
                }
            }
        }

        /// <summary>
        /// Fixes the normals.
        /// </summary>
        private void FixNormals()
        {
            for (int i = 0; i < indices.Length; i += 3)
            {
                Vector3 v1 = vertices[indices[i + 1]].Position - vertices[indices[i]].Position;
                Vector3 v2 = vertices[indices[i + 2]].Position - vertices[indices[i]].Position;
                Vector3 normal;
                Vector3.Cross(ref v2, ref v1, out normal);
                normal.Normalize();
                vertices[indices[i]].Normal += normal;
                vertices[indices[i + 1]].Normal += normal;
                vertices[indices[i + 2]].Normal += normal;
            }

            foreach (VertexPositionNormalTexture v in vertices)
            {
                v.Normal.Normalize();
            }
        }

        /// <summary>
        /// Initializes the indices.
        /// </summary>
        /// <param name="heigth">The heigth.</param>
        /// <param name="width">The width.</param>
        private void InitializeIndices(int heigth, int width)
        {
            indices = new int[(int)((Math.Ceiling((decimal)(vertices.Length + 1 / 3))) * 6)];

            int indicesIndex = 0;

            for (int i = 0; i < heigth - 1; i++)
            {
                for (int j = 0; j < width - 1; j++)
                {

                    if ((((i % 2 == 0) && (j % 2 == 0)) || ((i % 2 == 1) && (j % 2 == 1))) && ((i < heigth - 1) || (j < width - 1))) // draw 0, 1, 2    0, 2, 3
                    {
                        // Draw (J,I), (J+1, I), (J, I+1)    and then (J+1, I), (J+1, I+1), (J, I+1)
                        // Top most triangle
                        indices[indicesIndex++] = (numberAt(j, i, width - 1) - 1); // subtract 1 on each as the index starts at 0
                        indices[indicesIndex++] = (numberAt(j + 1, i, width - 1) - 1);
                        indices[indicesIndex++] = (numberAt(j, i + 1, width - 1) - 1);


                        // bottom most triangle
                        indices[indicesIndex++] = (numberAt(j + 1, i, width - 1) - 1);
                        indices[indicesIndex++] = (numberAt(j + 1, i + 1, width - 1) - 1);
                        indices[indicesIndex++] = (numberAt(j, i + 1, width - 1) - 1);

                    }
                    else if (((i < heigth - 1) || (j < width - 1))) // draw 1, 2, 3   0, 1, 3
                    {
                        // Draw (J, I), (J+1, I), (J+1, I+1)   and then (J, I), (J+1, I+1), (J, I+1)
                        // Top most triangle
                        indices[indicesIndex++] = (numberAt(j, i, width - 1) - 1);
                        indices[indicesIndex++] = (numberAt(j + 1, i, width - 1) - 1);
                        indices[indicesIndex++] = (numberAt(j + 1, i + 1, width - 1) - 1);


                        // bottom most triangle
                        indices[indicesIndex++] = (numberAt(j, i, width - 1) - 1);
                        indices[indicesIndex++] = (numberAt(j + 1, i + 1, width - 1) - 1);
                        indices[indicesIndex++] = (numberAt(j, i + 1, width - 1) - 1);
                    }
                }
            }
            Console.Write(indices.ToString());
        }


        /// <summary>
        /// Gets the number that should be at the row and column.
        /// </summary>
        /// <param name="j">The column.</param>
        /// <param name="i">The row.</param>
        /// <param name="maxJ">The number of columns.</param>
        /// <returns></returns>
        private int numberAt(int j, int i, int maxJ)
        {
            return (((i + 1) * (j + 1)) + ((maxJ - j) * i));
        }

        /// <summary>
        /// Changes the radius.
        /// </summary>
        /// <param name="radiusChange">The radius change.</param>
        public void ChangeRadius(float radiusChange)
        {
            radius += radiusChange;
            this.Initialize();
        }

        /// <summary>
        /// Increases the scale by the factor increaseBy.
        /// </summary>
        /// <param name="increaseBy">The increaseBy Factor.</param>
        public void IncreaseScale(float increaseBy)
        {
            SphereScale += increaseBy;
        }

        /// <summary>
        /// Decreases the scale.
        /// </summary>
        /// <param name="decreaseBy">The decrease by scale.</param>
        public void DecreaseScale(float decreaseBy)
        {
            SphereScale -= decreaseBy;
        }

        /// <summary>
        /// Turns the flat surface to a sphere.
        /// </summary>
        /// <param name="radius">The wanted radius.</param>
        /// <param name="xMax">The heigth of the flat surface.</param>
        /// <param name="yMax">The width of the flat surface.</param>
        private void Spherify(float radius, float xMax, float yMax)
        {
            yMax -= 1;
            xMax -= 1;

            for (int i = 0; i < vertices.Length; i++)
            {
                Vector3 old = vertices[i].Position;
                float ringradius = radius * ((float)Math.Sin(old.Z * Math.PI / yMax) + (float)Math.Sin(old.Y * Math.PI / yMax));


                Vector3 xyz = new Vector3((float)Math.Cos((xMax - old.X) * Math.PI * 2.0f / xMax) *
                    ringradius, (float)Math.Cos(old.Y * Math.PI / yMax) * radius, (float)Math.Sin((xMax - old.X) *
                    Math.PI * 2.0f / xMax) * ringradius);
                vertices[i].Position = xyz;
            }
        }

        /// <summary>
        /// Allows the game component to update itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        public override void Update(GameTime gameTime)
        {
            // TODO: Add your update code here
            objectWorld *= Matrix.CreateScale(SphereScale);
            SphereScale = 1.0f;
            base.Update(gameTime);
        }


        /// <summary>
        /// Draws the specified surface using the basic effect.
        /// </summary>
        /// <param name="basicEffect">The basic effect.</param>
        public void Draw(BasicEffect basicEffect)
        {
            basicEffect.World = objectWorld;
            basicEffect.Texture = heightMap;
            foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes)
            {
                pass.Apply();
            }

            parent.GraphicsDevice.SetVertexBuffer(vb);
            parent.GraphicsDevice.Indices = ib;

            parent.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, indices.Length, 0, indices.Length / 3);

        }

        /// <summary>
        /// Loads the height data.
        /// </summary>
        /// <param name="heightMap">The height map.</param>
        private void LoadHeightData(Texture2D heightMap)
        {
            width = heightMap.Width;
            heigth = heightMap.Height;

            Color[] heightMapColors = new Color[width * heigth];
            heightMap.GetData(heightMapColors);
            heightData = new HSL[width, heigth];
            for (int x = 0; x < width; x++)
                for (int y = 0; y < heigth; y++)
                    heightData[x, y] = RGB2HSL(heightMapColors[x + y * width]); //R / 5.0f;
        }

        /// <summary>
        /// Turns the RGB color c1 to the corrensponding HSL color.
        /// </summary>
        /// <param name="c1">The c1.</param>
        /// <returns></returns>
        private HSL RGB2HSL(Color c1)
        {
            double themin, themax, delta;
            HSL c2;
            themin = Math.Min(c1.R, Math.Min(c1.G, c1.B));
            themax = Math.Max(c1.R, Math.Max(c1.G, c1.B));
            delta = themax - themin;
            c2.l = (themin + themax) / 2;
            c2.s = 0;
            if (c2.l > 0 && c2.l < 1)
                c2.s = delta / (c2.l < 0.5 ? (2 * c2.l) : (2 - 2 * c2.l));
            c2.h = 0;
            if (delta > 0)
            {
                if (themax == c1.R && themax != c1.G)
                    c2.h += (c1.G - c1.B) / delta;
                if (themax == c1.G && themax != c1.B)
                    c2.h += (2 + (c1.B - c1.R) / delta);
                if (themax == c1.B && themax != c1.R)
                    c2.h += (4 + (c1.R - c1.G) / delta);
                c2.h *= 60;
            }
            return (c2);
        }


        /// <summary>
        /// Gets a random color.
        /// </summary>
        /// <returns></returns>
        private Color GetRandomColor()
        {
            return new Color(r.Next(250), r.Next(255), r.Next(250), 0);
        }

        /// <summary>
        /// Gets the surface radius.
        /// </summary>
        /// <returns></returns>
        public float GetSurfaceRadius()
        {
            return radius;
        }

        /// <summary>
        /// Gets the height data.
        /// </summary>
        /// <returns></returns>
        public HSL[,] GetHeightData()
        {
            return this.heightData;
        }
    }

}
